<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - three-gpu-pathtracer</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
		<style>
			body {
				color: #444;
				background-color: white;
			}

			a {
				color: #fb8c00;
			}

			.checkerboard {
				background-image:
					linear-gradient(45deg, #ddd 25%, transparent 25%),
					linear-gradient(-45deg, #ddd 25%, transparent 25%),
					linear-gradient(45deg, transparent 75%, #ddd 75%),
					linear-gradient(-45deg, transparent 75%, #ddd 75%);
				background-size: 20px 20px;
				background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
			}

			.lil-gui .gui-render {
				line-height: var(--widget-height);
				padding: var(--padding);
			}
		</style>
	</head>

	<body>
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> pathtracer - <a href="https://github.com/gkjohnson/three-gpu-pathtracer" target="_blank" rel="noopener">three-gpu-pathtracer</a><br/>
			See <a href="https://github.com/gkjohnson/three-gpu-pathtracer" target="_blank" rel="noopener">main project repository</a> for more information and examples on high fidelity path tracing.
		</div>

		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/",
					"three/examples/": "./",
					"three-gpu-pathtracer": "https://cdn.jsdelivr.net/npm/three-gpu-pathtracer@0.0.22/build/index.module.js",
					"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.7.4/build/index.module.js"
				}
			}
		</script>

		<script type="module">

			import * as THREE from 'three';

			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
			import { LDrawLoader } from 'three/addons/loaders/LDrawLoader.js';
			import { LDrawUtils } from 'three/addons/utils/LDrawUtils.js';

			import { WebGLPathTracer, BlurredEnvMapGenerator, GradientEquirectTexture } from 'three-gpu-pathtracer';

			let progressBarDiv, samplesEl;
			let camera, scene, renderer, controls, gui;
			let pathTracer, floor, gradientMap;

			const params = {
				enable: true,
				toneMapping: true,
				pause: false,
				tiles: 3,
				transparentBackground: false,
				resolutionScale: 1,
				download: () => {

					const link = document.createElement( 'a' );
					link.download = 'pathtraced-render.png';
					link.href = renderer.domElement.toDataURL().replace( 'image/png', 'image/octet-stream' );
					link.click();

				},
				roughness: 0.15,
				metalness: 0.9,
			};

			init();

			function init() {

				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 10000 );
				camera.position.set( 150, 200, 250 );

				// initialize the renderer
				renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true, preserveDrawingBuffer: true, premultipliedAlpha: false } );
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer.toneMapping = THREE.ACESFilmicToneMapping;
				document.body.appendChild( renderer.domElement );

				gradientMap = new GradientEquirectTexture();
				gradientMap.topColor.set( 0xeeeeee );
				gradientMap.bottomColor.set( 0xeaeaea );
				gradientMap.update();

				// initialize the pathtracer
				pathTracer = new WebGLPathTracer( renderer );
				pathTracer.filterGlossyFactor = 1;
				pathTracer.minSamples = 3;
				pathTracer.renderScale = params.resolutionScale;
				pathTracer.tiles.set( params.tiles, params.tiles );

				// scene
				scene = new THREE.Scene();
				scene.background = gradientMap;

				controls = new OrbitControls( camera, renderer.domElement );
				controls.addEventListener( 'change', () => {

					pathTracer.updateCamera();

				} );

				window.addEventListener( 'resize', onWindowResize );
				onWindowResize();

				progressBarDiv = document.createElement( 'div' );
				progressBarDiv.innerText = 'Loading...';
				progressBarDiv.style.fontSize = '3em';
				progressBarDiv.style.color = '#888';
				progressBarDiv.style.display = 'block';
				progressBarDiv.style.position = 'absolute';
				progressBarDiv.style.top = '50%';
				progressBarDiv.style.width = '100%';
				progressBarDiv.style.textAlign = 'center';

				// load materials and then the model
				createGUI();

				loadModel();

			}

			async function loadModel() {

				progressBarDiv.innerText = 'Loading...';

				let model = null;
				let environment = null;

				updateProgressBar( 0 );
				showProgressBar();

				// only smooth when not rendering with flat colors to improve processing time
				const ldrawPromise =
					new LDrawLoader()
						.setPath( 'models/ldraw/officialLibrary/' )
						.loadAsync( 'models/7140-1-X-wingFighter.mpd_Packed.mpd', onProgress )
						.then( function ( legoGroup ) {

							// Convert from LDraw coordinates: rotate 180 degrees around OX
							legoGroup = LDrawUtils.mergeObject( legoGroup );
							legoGroup.rotation.x = Math.PI;
							legoGroup.updateMatrixWorld();
							model = legoGroup;

							legoGroup.traverse( c => {

								// hide the line segments
								if ( c.isLineSegments ) {

									c.visible = false;

								}

								// adjust the materials to use transmission, be a bit shinier
								if ( c.material ) {

									c.material.roughness *= 0.25;

									if ( c.material.opacity < 1.0 ) {

										const oldMaterial = c.material;
										const newMaterial = new THREE.MeshPhysicalMaterial();

										newMaterial.opacity = 1.0;
										newMaterial.transmission = 1.0;
										newMaterial.thickness = 1.0;
										newMaterial.ior = 1.4;
										newMaterial.roughness = oldMaterial.roughness;
										newMaterial.metalness = 0.0;

										const hsl = {};
										oldMaterial.color.getHSL( hsl );
										hsl.l = Math.max( hsl.l, 0.35 );
										newMaterial.color.setHSL( hsl.h, hsl.s, hsl.l );

										c.material = newMaterial;

									}

								}

							} );

						} )
						.catch( onError );

				const envMapPromise =
					new RGBELoader()
						.setPath( 'textures/equirectangular/' )
						.loadAsync( 'royal_esplanade_1k.hdr' )
						.then( tex => {

							const envMapGenerator = new BlurredEnvMapGenerator( renderer );
							const blurredEnvMap = envMapGenerator.generate( tex, 0 );

							environment = blurredEnvMap;

						} )
						.catch( onError );

				await Promise.all( [ envMapPromise, ldrawPromise ] );

				hideProgressBar();
				document.body.classList.add( 'checkerboard' );

				// set environment map
				scene.environment = environment;

				// Adjust camera
				const bbox = new THREE.Box3().setFromObject( model );
				const size = bbox.getSize( new THREE.Vector3() );
				const radius = Math.max( size.x, Math.max( size.y, size.z ) ) * 0.4;

				controls.target0.copy( bbox.getCenter( new THREE.Vector3() ) );
				controls.position0.set( 2.3, 1, 2 ).multiplyScalar( radius ).add( controls.target0 );
				controls.reset();

				// add the model
				scene.add( model );

				// add floor
				floor = new THREE.Mesh(
					new THREE.PlaneGeometry(),
					new THREE.MeshStandardMaterial( {
						side: THREE.DoubleSide,
						roughness: params.roughness,
						metalness: params.metalness,
						map: generateRadialFloorTexture( 1024 ),
						transparent: true,
					} ),
				);
				floor.scale.setScalar( 2500 );
				floor.rotation.x = - Math.PI / 2;
				floor.position.y = bbox.min.y;
				scene.add( floor );

				// reset the progress bar to display bvh generation
				progressBarDiv.innerText = 'Generating BVH...';
				updateProgressBar( 0 );

				pathTracer.setScene( scene, camera );
			
				renderer.setAnimationLoop( animate );

			}

			function onWindowResize() {

				const w = window.innerWidth;
				const h = window.innerHeight;
				const dpr = window.devicePixelRatio;

				renderer.setSize( w, h );
				renderer.setPixelRatio( dpr );

				const aspect = w / h;
				camera.aspect = aspect;
				camera.updateProjectionMatrix();

				pathTracer.updateCamera();

			}

			function createGUI() {

				if ( gui ) {

					gui.destroy();

				}

				gui = new GUI();
				gui.add( params, 'enable' );
				gui.add( params, 'pause' );
				gui.add( params, 'toneMapping' );
				gui.add( params, 'transparentBackground' ).onChange( v => {

					scene.background = v ? null : gradientMap;
					pathTracer.updateEnvironment();

				} );
				gui.add( params, 'resolutionScale', 0.1, 1.0, 0.1 ).onChange( v => {

					pathTracer.renderScale = v;
					pathTracer.reset();

				} );
				gui.add( params, 'tiles', 1, 6, 1 ).onChange( v => {

					pathTracer.tiles.set( v, v );

				} );
				gui.add( params, 'roughness', 0, 1 ).name( 'floor roughness' ).onChange( v => {

					floor.material.roughness = v;
					pathTracer.updateMaterials();

				} );
				gui.add( params, 'metalness', 0, 1 ).name( 'floor metalness' ).onChange( v => {

					floor.material.metalness = v;
					pathTracer.updateMaterials();

				} );
				gui.add( params, 'download' ).name( 'download image' );

				const renderFolder = gui.addFolder( 'Render' );

				samplesEl = document.createElement( 'div' );
				samplesEl.classList.add( 'gui-render' );
				samplesEl.innerText = 'samples: 0';

				renderFolder.$children.appendChild( samplesEl );
				renderFolder.open();

			}

			//

			function animate() {

				renderer.toneMapping = params.toneMapping ? THREE.ACESFilmicToneMapping : THREE.NoToneMapping;

				const samples = Math.floor( pathTracer.samples );
				samplesEl.innerText = `samples: ${ samples }`;

				pathTracer.enablePathTracing = params.enable;
				pathTracer.pausePathTracing = params.pause;
				pathTracer.renderSample();

				samplesEl.innerText = `samples: ${ Math.floor( pathTracer.samples ) }`;

			}

			function onProgress( xhr ) {

				if ( xhr.lengthComputable ) {

					updateProgressBar( xhr.loaded / xhr.total );

					console.log( Math.round( xhr.loaded / xhr.total * 100, 2 ) + '% downloaded' );

				}

			}

			function onError( error ) {

				const message = 'Error loading model';
				progressBarDiv.innerText = message;
				console.log( message );
				console.error( error );

			}

			function showProgressBar() {

				document.body.appendChild( progressBarDiv );

			}

			function hideProgressBar() {

				document.body.removeChild( progressBarDiv );

			}

			function updateProgressBar( fraction ) {

				progressBarDiv.innerText = 'Loading... ' + Math.round( fraction * 100, 2 ) + '%';

			}

			function generateRadialFloorTexture( dim ) {

				const data = new Uint8Array( dim * dim * 4 );

				for ( let x = 0; x < dim; x ++ ) {

					for ( let y = 0; y < dim; y ++ ) {

						const xNorm = x / ( dim - 1 );
						const yNorm = y / ( dim - 1 );

						const xCent = 2.0 * ( xNorm - 0.5 );
						const yCent = 2.0 * ( yNorm - 0.5 );
						let a = Math.max( Math.min( 1.0 - Math.sqrt( xCent ** 2 + yCent ** 2 ), 1.0 ), 0.0 );
						a = a ** 1.5;
						a = a * 1.5;
						a = Math.min( a, 1.0 );

						const i = y * dim + x;
						data[ i * 4 + 0 ] = 255;
						data[ i * 4 + 1 ] = 255;
						data[ i * 4 + 2 ] = 255;
						data[ i * 4 + 3 ] = a * 255;

					}

				}

				const tex = new THREE.DataTexture( data, dim, dim );
				tex.format = THREE.RGBAFormat;
				tex.type = THREE.UnsignedByteType;
				tex.minFilter = THREE.LinearFilter;
				tex.magFilter = THREE.LinearFilter;
				tex.wrapS = THREE.RepeatWrapping;
				tex.wrapT = THREE.RepeatWrapping;
				tex.needsUpdate = true;
				return tex;

			}

		</script>

		<!-- LDraw.org CC BY 2.0 Parts Library attribution -->
		<div style="display: block; position: absolute; bottom: 8px; left: 8px; width: 160px; padding: 10px; background-color: #F3F7F8;">
			<center>
				<a href="http://www.ldraw.org"><img style="width: 145px" src="models/ldraw/ldraw_org_logo/Stamp145.png"></a>
				<br />
				<a href="http://www.ldraw.org/">This software uses the LDraw Parts Library</a>
			</center>
		</div>

	</body>
</html>
